home *** CD-ROM | disk | FTP | other *** search
- # -*- coding: utf-8 -*-
- """
- """
-
- __author__ = 'Robert Ancell <bob27@users.sourceforge.net>'
- __license__ = 'GNU General Public License Version 2'
- __copyright__ = 'Copyright 2005-2006 Robert Ancell'
-
- __all__ = ['SANConverter']
-
- # Examples of SAN moves:
- #
- # f4 (pawn move to f4)
- # fxg3 (pawn on file f takes opponent on g3)
- # Qh5 (queen moves to h5)
- # Qh5+ (queen moves to h5 and puts opponent into check)
- # Ned4 (knight on file e moves to d4)
- # gxh8=Q# (pawn on g7 takes opponent in h8 promotes to queen and puts oponent into checkmate (smooth!))
-
- # Notation for takes
- SAN_TAKE = 'x'
-
- # Castling moves
- SAN_CASTLE_SHORT = 'O-O'
- SAN_CASTLE_LONG = 'O-O-O'
-
- RANKS = 'abcdefgh'
- FILES = '12345678'
-
- # Suffixes
- SAN_PROMOTE = '='
-
- class Error(Exception):
- """
- """
- pass
-
- class SANConverter:
- """
-
- Define file and rank
- """
-
- # Piece colours
- WHITE = 'White'
- BLACK = 'Black'
-
- # SAN piece types
- PAWN = 'P'
- KNIGHT = 'N'
- BISHOP = 'B'
- ROOK = 'R'
- QUEEN = 'Q'
- KING = 'K'
- __pieceTypes = PAWN + KNIGHT + BISHOP + ROOK + QUEEN + KING
-
- # Valid promotion types
- __promotionTypes = PAWN + KNIGHT + BISHOP + ROOK + QUEEN
-
- # Move results
- CHECK = '+'
- CHECKMATE = '#'
-
- def __init__(self):
- """Constructor"""
- pass
-
- # Methods to extend
-
- def getPiece(self, location):
- """Get a piece from the chess board.
-
- 'location' is the location to get the piece from (string, e.g. 'a1', h8').
-
- Return a tuple containing (colour, type) or None if no piece at this location.
- """
- return None
-
- def testMove(self, colour, start, end, promotionType, allowSuicide = False):
- """Test if a move is valid.
-
- 'colour' is the colour of the player making the move (self.WHITE or self.BLACK).
- 'start' is the board location to move from (string, e.g. 'a1', 'h8').
- 'end' is the board location to move to (string, e.g. 'a1', 'h8').
- 'promotionType' is the piece type to promote to (self.[PAWN|KNIGHT|BISHOP|ROOK|QUEEN]).
- 'allowSuicide' is a flag to show if the move should be disallowed (False) or
- allowed (True) if it would put the moving player into check.
-
- Return False if the move is dissallowed or
- self.CHECK if the move puts the opponent into check or
- self.CHECKMATE if the move puts the opponent into checkmate or
- True if the move is allowed and does not put the opponent into check.
- """
- pass
-
- def decode(self, colour, san):
- """Decode a SAN move.
-
- 'colour' is the colour of the player making the move (self.WHITE or self.BLACK).
- 'san' is the SAN description of the move (string).
-
- Returns the move this SAN describes in the form (start, end, promotionType).
- 'start' is the square to move from (string, e.g. 'a1', 'h8').
- 'end' is the square to move to (string, e.g. 'a1', 'h8').
- 'promotionType' is the piece to promote to (self.[KNIGHT|BISHOP|ROOK|QUEEN]).
- If the move is invalid then an Error expection is raised.
- """
- copy = san[:]
-
- # Look for check hints
- expectedResult = True
- if copy[-1] == self.CHECK or copy[-1] == self.CHECKMATE:
- expectedResult = copy[-1]
- copy = copy[:-1]
-
- # Extract promotions
- promotionType = self.QUEEN
- if copy[-2] == SAN_PROMOTE:
- promotionType = copy[-1]
- copy = copy[:-2]
- if self.__promotionTypes.find(promotionType) < 0:
- raise Error("Error decoding '%s', Invalid promotion type %s" % (repr(san), repr(promotionType)))
-
- # Some people miss out the '='
- elif self.__promotionTypes.find(copy[-1]) >= 0:
- promotionType = copy[-1]
- copy = copy[:-1]
-
- # Check for castling moves
- if colour is self.WHITE:
- baseFile = '1'
- else:
- baseFile = '8'
- # FIXME: Update moveResult and compare against expectedResult
- if copy == SAN_CASTLE_SHORT:
- return ('e' + baseFile, 'g' + baseFile, expectedResult, promotionType)
- elif copy == SAN_CASTLE_LONG:
- return ('e' + baseFile, 'c' + baseFile, expectedResult, promotionType)
-
- # Get the destination (the last two characters before the suffix)
- end = copy[-2:]
- copy = copy[:-2]
- if RANKS.find(end[0]) < 0 or FILES.find(end[1]) < 0:
- raise Error("Error decoding '%s', Invalid destination type %s" % (repr(san), repr(end)))
-
- # Check if is a take move (use try in case there are no more characters)
- isTake = False
- try:
- if copy[-1] == SAN_TAKE:
- isTake = True
- copy = copy[:-1]
- except:
- pass
-
- # The first character is the piece type (or pawn if not specified)
- pieceType = self.PAWN
- if len(copy) > 0:
- if self.__pieceTypes.find(copy[0]) >= 0:
- pieceType = copy[0]
- copy = copy[1:]
-
- # Get the rank of the source piece (if supplied)
- rank = None
- if len(copy) > 0:
- if RANKS.find(copy[0]) >= 0:
- rank = copy[0]
- copy = copy[1:]
-
- # Get the file of the source piece (if supplied)
- file = None
- if len(copy) > 0:
- if FILES.find(copy[0]) >= 0:
- file = copy[0]
- copy = copy[1:]
-
- # There should be no more characters
- if len(copy) != 0:
- raise Error('Error decoding %s, Unexpected extra characters %s' % (repr(san), repr(end)))
-
- # If have both rank and file for source then we have the move completely defined
- moveResult = None
- move = None
- if rank is not None and file is not None:
- start = rank + file
- moveResult = self.testMove(colour, start, end, promotionType = promotionType)
- move = (start, end)
- else:
- # Try and find a piece that matches the source one
- if file is None:
- fileRange = FILES
- else:
- fileRange = file
- if rank is None:
- rankRange = RANKS
- else:
- rankRange = rank
-
- for file in fileRange:
- for rank in rankRange:
- start = rank + file
-
- # Only test our pieces
- piece = self.getPiece(start)
- if piece is None:
- continue
- if piece[0] != colour or piece[1] != pieceType:
- continue
-
- # If move is valid this is a possible move
- # FIXME: Check the crafty case in reverse (i.e. suicidal moves)
- result = self.testMove(colour, start, end, promotionType = promotionType)
- if result is False:
- continue
-
- # Multiple matches
- if moveResult is not None:
- raise Error('Error decoding %s, Move is ambiguous, at least %s and %s are possible' % (repr(san), repr(move), repr([start, end])))
- moveResult = result
- move = [start, end]
-
- # Failed to find a match
- if moveResult is None:
- raise Error('Error decoding %s, Not a valid move' % repr(san))
-
- return (move[0], move[1], expectedResult, promotionType)
-
- def encode(self, start, end, isTake = False, promotionType = QUEEN):
- """Convert glChess co-ordinate move to SAN notation.
-
- 'start' is the square to move from (string, e.g. 'a1', 'h8').
- 'end' is the square to move to (string, e.g. 'a1', 'h8').
- 'promotionType' is the piece used for pawn promotion (if necessary).
-
- Return the move in SAN notation or None if unable to convert.
- """
- piece = self.getPiece(start)
- if piece is None:
- raise Error('Encode error, no piece to move at %s' % repr(start))
- (pieceColour, pieceType) = piece
-
- # Test the move is valid
- if self.testMove(pieceColour, start, end, promotionType) is False:
- raise Error('Encode error, move %s%s is invalid' % (start, end))
-
- # Check for castling
- if pieceType is self.KING:
- # Castling
- if pieceColour is self.WHITE:
- baseFile = '1'
- else:
- baseFile = '8'
- shortCastle = ('e' + baseFile, 'g' + baseFile)
- longCastle = ('e' + baseFile, 'c' + baseFile)
-
- san = None
- if (start, end) == shortCastle:
- san = SAN_CASTLE_SHORT
- elif (start, end) == longCastle:
- san = SAN_CASTLE_LONG
- if san is not None:
- result = self.testMove(pieceColour, start, end, promotionType = promotionType, allowSuicide = True)
- if result is self.CHECK:
- san += self.CHECK
- elif result is self.CHECKMATE:
- san += self.CHECKMATE
- return san
-
- # Try and describe this piece with the minimum of information
- file = '?'
- rank = '?'
-
- # Pawns always explicitly provide rank when taking
- if pieceType is self.PAWN and isTake:
- rank = start[0]
-
- # First try no rank or file, then just file, then just rank, then both
- result = self.__isUnique(pieceColour, pieceType, rank + file, end, promotionType)
- if result is None:
- # Try with rank
- rank = start[0]
- file = '?'
- result = self.__isUnique(pieceColour, pieceType, rank + '?', end, promotionType)
-
- if result is None:
- # Try with file
- rank = '?'
- file = start[1]
- result = self.__isUnique(pieceColour, pieceType, '?' + file, end, promotionType)
-
- if result is None:
- # Try with rank and file
- result = self.__isUnique(pieceColour, pieceType, rank + file, end, promotionType)
-
- # This move is illegal
- if result is None:
- raise Error('Encode error, unable to find unique move for %s%s' % (start, end))
-
- # Store the piece that is being moved, note pawns are not marked
- san = ''
- if pieceType is not self.PAWN:
- san += pieceType
-
- # Disambiguations
- if rank != '?':
- san += rank
- if file != '?':
- san += file
-
- # Mark if taking a piece
- if isTake:
- san += SAN_TAKE
-
- # Write target co-ordinate
- san += end
-
- # If a pawn promotion record the type
- if pieceColour is self.WHITE:
- promotionFile = '8'
- else:
- promotionFile = '1'
- if pieceType == self.PAWN and end[1] == promotionFile:
- san += SAN_PROMOTE + promotionType
-
- # Record if this is a check/checkmate move
- if result is self.CHECK:
- san += self.CHECK
- elif result is self.CHECKMATE:
- san += self.CHECKMATE
-
- return san
-
- def __isUnique(self, colour, pieceType, start, end, promotionType = QUEEN):
- """Test if a move is unique.
-
- 'colour' is the piece colour being moved. (self.WHITE or self.BLACK).
- 'pieceType' is the type of the piece being moved (self.[PAWN|KNIGHT|BISHOP|ROOK|QUEEN|KING]).
- 'start' is the start location of the move (tuple (file, rank). rank and file can be None).
- 'end' is the end point of the move (tuple (file,rank)).
- 'promotionType' is the piece type to promote pawns to (self.[PAWN|KNIGHT|BISHOP|ROOK|QUEEN]).
-
- Return the result of self.testMove() if a unique move is found otherwise None.
- """
- lastResult = None
-
- # Work out what ranges to iterate over
- if start[0] == '?':
- rankRange = RANKS
- else:
- rankRange = start[0]
- if start[1] == '?':
- fileRange = FILES
- else:
- fileRange = start[1]
-
- for file in fileRange:
- for rank in rankRange:
- # Check if there is a piece of this type and colour at this location
- p = self.getPiece(rank + file)
- if p is None:
- continue
- if p[1] != pieceType or p[0] != colour:
- continue
-
- # If move is valid this is a possible move
- # NOTE: We check moves that would be suicide for us otherwise crafty claims they
- # are ambiguous, the PGN specification says we don't need to disambiguate if only
- # one non-suicidal move is available.
- # (8.2.3.4: Disambiguation) */
- result = self.testMove(colour, rank + file, end, promotionType = promotionType, allowSuicide = True)
- if result is not False:
- # If multiple matches then not unique (duh!)
- if lastResult != None:
- return None
- lastResult = result
-
- # Return the result of the move
- return lastResult
-
- if __name__ == '__main__':
-
- import chess_board
-
- class TestConverter(SANConverter):
- """
- """
-
- __colourToSAN = {chess_board.WHITE: SANConverter.WHITE, chess_board.BLACK: SANConverter.BLACK}
- __sanToColour = {}
- for (a, b) in __colourToSAN.iteritems():
- __sanToColour[b] = a
-
- __typeToSAN = {chess_board.PAWN: SANConverter.PAWN,
- chess_board.KNIGHT: SANConverter.KNIGHT,
- chess_board.BISHOP: SANConverter.BISHOP,
- chess_board.ROOK: SANConverter.ROOK,
- chess_board.QUEEN: SANConverter.QUEEN,
- chess_board.KING: SANConverter.KING}
- __sanToType = {}
- for (a, b) in __typeToSAN.iteritems():
- __sanToType[b] = a
-
- __board = None
-
- def __init__(self, board):
- self.__board = board
-
- def testEncode(self, start, end):
- print str((start, end)) + ' => ' + str(self.encode(start, end))
-
- def testDecode(self, colour, san):
- try:
- result = self.decode(colour, san)
- print san.ljust(7) + ' => ' + str(result)
- except Error, e:
- print san.ljust(7) + ' !! ' + str(e)
-
- def getPiece(self, file, rank):
- """Called by SANConverter"""
- piece = self.__board.getPiece((file, rank))
- if piece is None:
- return None
- return (self.__colourToSAN[piece.getColour()], self.__typeToSAN[piece.getType()])
-
- def testMove(self, colour, start, end, promotionType, allowSuicide = False):
- """Called by SANConverter"""
- moveResult = self.__board.testMove(self.__sanToColour[colour], ((start, end)), self.__sanToType[promotionType], allowSuicide)
-
- return {chess_board.MOVE_RESULT_ILLEGAL: False,
- chess_board.MOVE_RESULT_ALLOWED: True,
- chess_board.MOVE_RESULT_OPPONENT_CHECK: self.CHECK,
- chess_board.MOVE_RESULT_OPPONENT_CHECKMATE: self.CHECKMATE}[moveResult]
-
- b = chess_board.ChessBoard()
- c = TestConverter(b)
-
- print b
- c.testEncode((1,1), (1,2))
- c.testEncode((1,0), (2,2))
-
- c.testDecode(c.WHITE, 'c3') # Simple pawn move
- c.testDecode(c.WHITE, 'Pc3') # Explicit pawn move
- c.testDecode(c.WHITE, 'c4') # Pawn march
- c.testDecode(c.WHITE, 'Nc3') # Non-pawn move
- c.testDecode(c.WHITE, 'Qd3') # Invalid move
- c.testDecode(c.WHITE, 'Qd3=X') # Invalid promotion type
- c.testDecode(c.WHITE, 'x3') # Invalid destination
- c.testDecode(c.WHITE, 'ic3') # Extra characters
- # TODO: Ambiguous move
- print b
-